package com.easywork.jythonspring;

import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.python.core.Py;
import org.python.core.PyClass;
import org.python.core.PyInstance;
import org.python.core.PyList;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.util.PythonInterpreter;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.config.AbstractFactoryBean;
/**
 * Python factory bean object.
 * <p>
 * This object help you to register a python object into spring bean context.
 * </p>
 * <p>
 * User must define the bean's name as the python file you want to use as a java bean.
 * It accept both class path or absolute file path.
 * </p>
 * @author justicejiang
 *
 */
public class PyFactoryBean extends AbstractFactoryBean implements BeanNameAware{
	//name of the bean
	protected String name;
	//interfaces that this bean can be casted to
	protected Class<?>[] interfaceClasses;
	//class shortcut to the python file
	protected PyClass pyClass;
	//instance shortcut to the python file
	protected PyInstance instance;
	//bean properties to set up from spring bean context
	protected Map<String,Object> properties;
	//auto wire bean by property name
	protected boolean autowireByName = false;
	//Class name served as service class, default is the file's name
	protected String serviceClass;

	public void setProperties(Map<String, Object> properties) {
		this.properties = properties;
	}

	/**
	 * In this file, we'll try to get the python file declared in the bean definition.
	 * Further more, we'll look up a class define in it naming as the file's name.
	 * Also we'll load the interface classes that this bean can be casted to.
	 */
	@Override
	public void afterPropertiesSet() throws Exception {
		fixName();
		InputStream is = PyFactoryBean.class.getResourceAsStream(name);
		PythonInterpreter python = new PythonInterpreter();
		python.execfile(is);
		if(StringUtils.isBlank(serviceClass))
			serviceClass = name.substring(name.lastIndexOf("/")+1).replaceAll("\\.py", "");
		pyClass = (PyClass) python.get(serviceClass);
		instance = (PyInstance) pyClass.__call__();
		initPyInstance();
		if(interfaceClasses == null)
			interfaceClasses = getInterfaces();
		if(interfaceClasses == null)
			throw new RuntimeException("No interface for this bean! At lease one interface should this bean implemented!");
		super.afterPropertiesSet();
	}

	/*
	 * We'd created a proxy that will call the required method in the python instance.
	 * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance()
	 */
	@Override
	protected Object createInstance() throws Exception {
		return Proxy.newProxyInstance(
				PyFactoryBean.class.getClassLoader(), 
				interfaceClasses,
				new InvocationHandler(){
					private static final String METHOD_TO_STRING = "toString";
					private static final String METHOD_EQUALS = "equals";
					private static final String METHOD_GET_CLASS = "getClass";
					private static final String METHOD_HASH_CODE = "hashCode";
					
					
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						Object result = invokeObjectMethods(method.getName(),args);
						if(result != null)
							return result;
						PyObject pyMethod = instance.__getattr__(method.getName());
						Class<?> returnType = method.getReturnType();
						ArrayList<PyObject> params = new ArrayList<PyObject>();
						if(args != null && args.length >0)
							for(Object arg : args)
								params.add(Py.java2py(arg));
						PyObject pyResult = null;
						if(params.isEmpty())
							pyResult = pyMethod.__call__();
						else
							pyResult = pyMethod.__call__(params.toArray(new PyObject[0]));
						if(returnType != null)
							result = pyResult.__tojava__(returnType);
						return result;
					}
					
					private Object invokeObjectMethods(String methodName,Object[] args)
					{
						if(methodName.equals(METHOD_TO_STRING))
							return instance.toString();
						else if(methodName.equals(METHOD_GET_CLASS))
							return instance.getClass();
						else if(methodName.equals(METHOD_EQUALS))
							return instance.equals(args[0]);
						else if(methodName.equals(METHOD_HASH_CODE))
							return instance.hashCode();
						else
							return null;						
					}
				}
		);
	}

	@SuppressWarnings("unchecked")
	@Override
	public Class getObjectType() {
		return null;
	}

	public void setBeanName(String name) {
		this.name = name;
	}

	public void setInterfaceClasses(Class<?>[] interfaceClasses) {
		this.interfaceClasses = interfaceClasses;
	}
	
	/*
	 * Get interface classes as the python bean implemented for the proxy
	 */
	private Class<?>[] getInterfaces()
	{
		if(StringUtils.isBlank(name))
			throw new RuntimeException("No bean name is define for this bean, please define a bean name attribute to indicates the reference python file");
		ArrayList<Class<?>> interfaceClasses = new ArrayList<Class<?>>();
		for(int i=0;i<pyClass.__bases__.__len__();i++)
		{
			Class<?> anInterfaceClass = (Class<?>) pyClass.__bases__.get(i);
			if(anInterfaceClass.isInterface())
				interfaceClasses.add(anInterfaceClass);
		}
		return interfaceClasses.toArray(new Class<?>[0]);
	}
	
	/*
	 * Initialized python instance
	 */
	private void initPyInstance() {
		if(autowireByName)
		{
			//auto wire bean by set methods
			PyList dic = (PyList) pyClass.__dir__();
			String methodName,beanName;
			for(int i=0;i<dic.__len__();i++)
			{
				methodName = dic.__getitem__(i).toString();
				if(methodName.startsWith("set") && methodName.length() > 3)
				{
					beanName = methodName.replaceFirst("set", "");
					beanName = StringUtils.uncapitalize(beanName);
					Object bean = getBeanFactory().getBean(beanName);
					if(bean != null)
						instance.__getattr__(new PyString(methodName)).__call__(Py.java2py(bean));
				}
			}
		}
		else if (properties!= null && !properties.isEmpty())
		{
			//wire bean by specific properties
			String setMethodName;
			for(Map.Entry<String,Object> property : properties.entrySet())
			{
				setMethodName = "set"+StringUtils.capitalize(property.getKey());
				instance.__getattr__(new PyString(setMethodName)).__call__(Py.java2py(property.getValue()));
			}
		}
	}
	
	/*
	 * convert bean id into the name property if defined 
	 */
	private void fixName() {
		String[] alias = getBeanFactory().getAliases(name);
		if(alias != null && alias.length >0)
			name = alias[0];
	}

	public void setAutowireByName(boolean autowireByName) {
		this.autowireByName = autowireByName;
	}

	public void setServiceClass(String serviceClass) {
		this.serviceClass = serviceClass;
	}
}
